<?php
/**
 * In this file the exception class '\BF\Security' is defined.
 *
 * @author     Kado <ulfikado@gmail.com>
 * @category   Beluga PHP5.3+ Framework
 * @package    BF\Core
 * @since      2015-01-17 14:29
 * @subpackage Security
 * @version    0.1
 */

namespace BF
{

   include_once __DIR__ . '/PasswordDeriveBytes.class.php';

   /**
    * This class defines a object with some static security methods.
    *
    * @since      v0.1
    */
   class Security
   {

      # <editor-fold defaultstate="collapsed" desc=" - - -   P R I V A T E   S T A T I C   F I E L D S   - - - - - - - - - - - - - - -">

      private static $alphaChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_';
      private static $wordChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_';
      private static $PassBytes = array(
         0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 0x65, 0x64, 0x65, 0x64,
         0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76, 0x65, 0x64, 0x65,
         0x61, 0x6e, 0x49, 0x76, 0x4d, 0x65, 0x64, 0x65, 0x76, 0x65
      );
      private static $lastRandoms = array();

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - -   P U B L I C   S T A T I C   M E T H O D S   - - - - - - - - - - - - - - -">

      /**
       * Creates a word at random determined characters from range [A-Za-z0-9_], always starting with a alphabethic
       * character or '_'. The the resulting word length is a random value between $minlength and $maxlength
       *
       * @param  int $minlength The minimal length of the word to generate.
       * @param  int $maxlength The maximal length of the word to generate.
       * @return string The generated word.
       */
      public static function BuildRandomWord( $minlength = 5, $maxlength = 12 )
      {

         // Define the max allowed index for some word characters
         $maxidx    = \strlen( self::$wordChars ) - 1;
         // Define the max allowed index for some alphanumeric characters
         $maxidx1   = \strlen( self::$alphaChars ) - 1;

         // Initialize the resulting value variable
         $res       = '';
         // and the current length of the resulting string
         $reslength = 0;
         $len       = $minlength;

         // Set it to 2 (the min allowed length) if its lower than 2
         if ( $len < 2 )
         {
            $len = 2;
         }
         if ( $maxlength > $len )
         {
            $len = \mt_rand( $len, $maxlength );
         }

         // Loop if it breaks, if a required condition is reached
         while ( true )
         {
            while ( $reslength < $len )
            {
               $res .= self::$wordChars[ \mt_rand( 0, $maxidx ) ];
               ++$reslength;
            }
            if ( !\preg_match( '~^[A-Za-z_].+$~', $res ) )
            {
               $res = self::$alphaChars[ \mt_rand( 0, $maxidx1 ) ] . \substr( $res, 1 );
            }
            if ( \in_array($res, self::$lastRandoms) )
            {
               $reslength = 0;
               continue;
            }
            self::$lastRandoms[] = $res;
            break;
         }

         return $res;

      }

      /**
       * Encrypts a string with a password.
       *
       * The used encryption algorithm is TripleDES.
       *
       * If you wand to decrypt strings, encrypted with this method, use {@see \BF\Security::Decrypt()}.
       *
       * @param  string $string                The string to encrypt
       * @param  string $password              The password to use for encryption
       * @param  boolean $trimBeforeEncrypt    Should the $string be trimmed before encryption?
       * @return string                        Returns the encrypted string as a string
       * @throws \BF\MissingExtensionException If the is no MCrypt extension loaded.
       * @throws \BF\Exception                 If the encryption fails
       */
      public static function Encrypt( $string, $password, $trimBeforeEncrypt = false )
      {

         if ( ! \MOD_MCRYPT )
         {
            throw new \BF\MissingExtensionException( 'MCrypt', 'Encryption fails.' );
         }

         // Trim by need
         if ( $trimBeforeEncrypt )
         {
            $string = \trim( $string );
         }

         // Empty string should returned unchanged!
         if ( empty( $string )
           || empty( $password ) )
         {
            return $string;
         }

         $pdb = new \BF\PasswordDeriveBytes( $password, self::$PassBytes );
         try
         {
            // The output buffering is required for catching some special mcrypt errors
            \ob_start();
            $td = \mcrypt_module_open( 'tripledes', '', 'ecb', '' );
            $iv = \mcrypt_create_iv( \mcrypt_enc_get_iv_size( $td ), \MCRYPT_RAND );
            \mcrypt_generic_init( $td, $pdb->getBytes( 24 ), $iv );
            $encrypted = \mcrypt_generic( $td, $string );
            \mcrypt_generic_deinit( $td );
            \mcrypt_module_close( $td );
            $err = \trim( \ob_get_contents() );
            // On Error trigger it
            if ( \strlen( $err ) > 0 )
            {
               throw new \Exception( \strip_tags($err) );
            }
            return \base64_encode( $encrypted );
         }
         catch ( \Exception $ex )
         {
            throw new \BF\Exception( 'Encrypting a string fails. ' . $ex->getMessage() );
         }

      }

      /**
       * Encrypts a prefix + string with a password.
       *
       * The used encryption algorithm is TripleDES.
       *
       * If you wand to decrypt strings, encrypted with this method, use {@see \BF\Security::DecryptPrefixed()}.
       *
       * @param  string $string                The string to encrypt
       * @param  string $password              The password to use for encryption
       * @param  string $prefix                The Prefix
       * @param  boolean $trimBeforeEncrypt    Should the $string be trimmed before encryption?
       * @return string                        Returns the encrypted string as a string
       * @throws \BF\MissingExtensionException If there is no MCrypt extension loaded.
       * @throws \BF\Exception                 If the encryption fails
       */
      public static function EncryptPrefixed( $string, $password, $prefix, $trimBeforeEncrypt = false )
      {
         return self::Encrypt( $prefix . $string, $password, $trimBeforeEncrypt );
      }

      /**
       * Decrypts a string with a password, encrypted before with {@see \BF\Security::Encrypt()}.
       *
       * @param  string $encrypted             The string to decrypt
       * @param  string $password              The password.
       * @return string                        Returns the decrypted string
       * @throws \BF\MissingExtensionException If there is no MCrypt extension loaded.
       * @throws \BF\Exception                 If the decryption fails
       */
      public static function Decrypt( $encrypted, $password )
      {
         if ( ! \MOD_MCRYPT )
         {
             throw new \BF\MissingExtensionException( 'MCrypt', 'Decryption fails.' );
         }
         if ( empty( $encrypted ) || empty( $password ) )
         {
            return $encrypted;
         }
         $pdb = new \BF\PasswordDeriveBytes( $password, self::$PassBytes );
         try
         {
            \ob_start();
            $td = \mcrypt_module_open( 'tripledes', '', 'ecb', '' );
            $iv = \mcrypt_create_iv( \mcrypt_enc_get_iv_size( $td ), \MCRYPT_RAND );
            \mcrypt_generic_init( $td, $pdb->getBytes( 24 ), $iv );
            $decrypted = \mdecrypt_generic( $td, \base64_decode( $encrypted ) );
            \mcrypt_generic_deinit( $td );
            \mcrypt_module_close( $td );
            $err = \trim( \ob_get_contents() );
            if ( \strlen( $err ) > 0 )
            {
               throw new \Exception( \strip_tags($err) );
            }
            return \trim( $decrypted, " \0" );
          }
          catch ( \Exception $ex )
          {
              throw new \BF\Exception( 'Decrypting a string fails. ' . $ex->getMessage() );
          }
      }

      /**
       * Decrypts a prefixed string with a password, encrypted before with {@see \BF\Security::EncryptPrefixed()}.
       *
       * @param  string $encrypted             The string to decrypt
       * @param  string $password              The password.
       * @param  string $prefix                Ther Prefix.
       * @return string|false                  Returns the decrypted string without the prefix, or bool FALSE if the
       *                                       prefix was not found.
       * @throws \BF\MissingExtensionException If there is no MCrypt extension loaded.
       * @throws \BF\Exception                 If the decryption fails
       */
      public static function DecryptPrefixed( $encrypted, $password, $prefix )
      {
         $res = self::Decrypt( $encrypted, $password );
         if ( ! \BF\str_startswith( $res, $prefix ) )
         {
            return false;
         }
         return \substr( $res, \strlen( $prefix ) );
      }

      # </editor-fold>

   }

}

